package com.ictravi.iamunit;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.StringTokenizer;
import junit.framework.AssertionFailedError;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.ddsteps.testcase.support.DDStepsExcelTestCase;
import com.ictravi.iamunit.connector.ConnectionConfigData;
import com.ictravi.iamunit.connector.Connector;
import com.ictravi.iamunit.ConfigLoader;
import com.ictravi.iamunit.TestStepExecute;

/**
 * IAMUnitTestCase class, the class that executes the test
 * 
 * @author Albert Louw (alouw@ictavi.com)
 * @version v1.0.0
 * @since v1.0.0
 **/
@SuppressWarnings("unused")
public class IAMUnitTestCase extends DDStepsExcelTestCase {
	private static Log LOG = LogFactory.getLog(IAMUnitTestCase.class);
	private Connector connector = null;
	private Map<String, Object> operationalDataMap;
	private Map<String, String> attributeMap;
	private ConnectionConfigData configData;
	// Constants for operation keys
	private String KEY_OPERATION= "operation";
	private String KEY_TARGET 	= "target";
	private String KEY_ENABLED 	= "disablestep";
	private String KEY_CRITICAL = "iscritical";
	private String KEY_WAIT 	= "waitinterval";
	private String KEY_RETRIES 	= "retrycount";
	private String KEY_FAIL 	= "expectfailure";
	private String KEY_INTERVAL = "WaitInterval";
	// Constants for operations
	private String OP_COMMENT 	= "comment";
	private String OP_ENCRYPT	= "encrypt";
	private String OP_WAIT 		= "wait";
	private String OP_DELAY 	= "delay";
	// Constants for parameters
	private String PARM_DELIM 	= "multivaldelimiter";

	// private String PARM_DELAY = "defaultdelay";

	/**
	 * cleans up a specific map
	 * 
	 * @since v1.0.0
	 * @param targetMap
	 *            to clean
	 **/
	private void cleanupMap(@SuppressWarnings("rawtypes") Map targetMap) {
		if (targetMap != null) {
			targetMap.clear();
			targetMap = null;
		}
	}

	/**
	 * cleans up the datamaps
	 * 
	 * @since v1.0.0
	 * @throws Exception
	 **/
	public void tearDownBeforeData() throws Exception {
		cleanupMap(this.attributeMap);
		cleanupMap(this.operationalDataMap);
	}

	/**
	 * getter for DDSteps for AttributeMap
	 * 
	 * @since v1.0.0
	 * @return Map AttributeMap with the attributes (all non //* properties)
	 **/
	public Map<String, String> getAttributeMap() {
		return attributeMap;
	}

	/**
	 * setter for the AttributeMap from DDSteps
	 * 
	 * @since v1.0.0
	 * @param attrMap
	 *            AttributeMap with the attributes (all non //* properties)
	 **/
	public void setAttributeMap(Map<String, String> attrMap) {
		if (this.attributeMap != null && this.attributeMap.size() > 0) {
			this.attributeMap.putAll(attrMap);
		} else {
			this.attributeMap = attrMap;
		}
	}

	/**
	 * getter for DDSteps for OperationalDataMap
	 * 
	 * @since v1.0.0
	 * @return Map OperationalDataMap with the Operational attributes (all //*
	 *         properties)
	 **/
	public Map<String, Object> getOperationalDataMap() {
		return operationalDataMap;
	}

	/**
	 * setter for the OperationalDataMap from DDSteps
	 * 
	 * @since v1.0.0
	 * @param dataMap
	 *            OperationalDataMap with the operational attributes
	 **/
	public void setOperationalDataMap(Map<String, Object> dataMap) {
		if (this.operationalDataMap != null
				&& this.operationalDataMap.size() > 0) {
			this.operationalDataMap.putAll(dataMap);
		} else {
			this.operationalDataMap = dataMap;
		}
	}

	/**
	 * sets up all data just before running the sheet
	 * 
	 * @since v1.0.0
	 * @throws Exception
	 **/
	public void setUpMethod() throws Exception {
		LOG.debug("...in: setUpMethod");
		// Get configuration info from file
		ConfigLoader.loadConfig("./iamunit-config.xml");
		// Get the setup information in the sheet if configured
		try {
			if (ConfigLoader.parms.get("loadsheetconfig").equalsIgnoreCase(
					"TRUE"))
				ConfigLoader.readConfig(this.getClass().getName());

		} catch (IAMUnitException e) {
			LOG.info("...readConfig: " + e.getMessage());
		}
		// Get the schema information in the sheet if configured
		try {
			if (ConfigLoader.parms.get("loadsheetschema").equalsIgnoreCase(
					"TRUE"))
				ConfigLoader.readSchema(this.getClass().getName());

		} catch (IAMUnitException e) {
			LOG.info("...readSchema: " + e.getMessage());
		}
		super.setUpMethod();
	}

	/**
	 * cleans up all data just before exiting the sheet
	 * 
	 * @since v1.0.0
	 * @throws Exception
	 **/
	public void tearDownMethod() throws Exception {
		LOG.debug("...in: tearDownMethod");
		super.tearDownMethod();
	}

	/**
	 * entry point for DDSteps to execute the test, allows for pre-test
	 * execution setup
	 * 
	 * @since v1.0.0
	 * @throws IAMUnitException
	 **/
	public void runTest() throws IAMUnitException {
		LOG.info("*****************************************************************************************************************************");
		LOG.debug("...in: runTest");
		executeTest();
		LOG.info("");
	}

	/**
	 * checks if the specified row in the test is disabled (//enabled = false)
	 * 
	 * @since v1.0.0
	 * @return boolean true if row is disabled, false is disabled
	 * @throws IAMUnitException
	 **/
	private boolean isRowEnabled() throws IAMUnitException {
		if (operationalDataMap.get(KEY_ENABLED) != null) {
			String ena = operationalDataMap.get(KEY_ENABLED).toString();
			if (ena.equalsIgnoreCase("TRUE"))
				return false;
			else
				return true;
		}
		return true;
	}

	private void initializeConfigData() throws IAMUnitException {
		String target = getOperationData(KEY_TARGET);
		// Loaded from XML
		configData = ConfigLoader.cmap.get(target);
		if (configData == null)
			throw new IAMUnitException(
					"IAM00030: Configuration not found. Make sure this target is setup");
	}

	/**
	 * executes the test according to the test data
	 * 
	 * @since v1.0.0
	 * @throws IAMUnitException
	 **/
	@SuppressWarnings("rawtypes")
	public void executeTest() throws IAMUnitException {
		LOG.debug("...in: executeTest");

		// Dump the parameters if debugging
		LOG.debug("Operations  : " + getOperationalDataMap().toString());
		LOG.debug("Attributes  : " + getAttributeMap().toString());

		// If this row is disabled, we're done
		if (isRowEnabled() == false) {
			LOG.info("...skipping this row, it's disabled");
			return;
		}

		if (getOperation().equalsIgnoreCase(OP_COMMENT)) {
			LOG.info("...comment: "
					+ (String) this.operationalDataMap.get(OP_COMMENT));
			return;
		}

		if (getOperation().equalsIgnoreCase(OP_ENCRYPT)) {
			LOG.info("pwd: "+attributeMap.get("dn"));
			EncTool encryptionManager = new EncTool("IAM-UNIT");
			System.out.println("Encrypted password value: " + encryptionManager.encryptCredentials(attributeMap.get("dn")));
			return;
		}

		if (getOperation().equalsIgnoreCase(OP_WAIT)
				|| getOperation().equalsIgnoreCase(OP_DELAY)) {
			LOG.info("...pausing: ");
			delay();
			return;
		}

		initializeConfigData();

		if (getOperation().equalsIgnoreCase("SCHEMA")) {
			ConfigLoader.setSchema(configData.getName(), attributeMap);
			LOG.info("Schema set for target: " + configData.getName());
			return;
		}

		if (getOperation().equalsIgnoreCase("TESTSCHEMA")) {
			Boolean schemaFound = false;
			Map<String, String> schema = new HashMap<String, String>();
			if (ConfigLoader.getSchema(configData.getName()) != null) {
				schemaFound = true;
				schema = stringToMap(ConfigLoader.getSchema(configData
						.getName()));
			}

			Iterator itr = attributeMap.keySet().iterator();
			while (itr.hasNext()) {
				String attrName = (String) itr.next().toString();
				String attrVal = (String) attributeMap.get(attrName).toString();
				if (schemaFound == true) {
					String mappedName = schema.get(attrName);
					if (mappedName == null) {
						LOG.info("Name: " + attrName + " value: " + attrVal);
					} else {
						LOG.info("Name: " + mappedName + " value: " + attrVal);
					}
				} else {
					LOG.info("Name: " + attrName + " value: " + attrVal);
				}
			}
			return;
		}

		// Setup the target connector
		connector = setupConnector((String) this.operationalDataMap
				.get(KEY_TARGET));

		try {
			for (long retriesRemaining = getRetryCount() - 1;; --retriesRemaining) {
				try {
					try {
						LOG.info(this.getName() + ": "
								+ getOperationData(OP_COMMENT));

						new TestStepExecute(operationalDataMap, connector,
								getOperation(), convertData()).runStep();
					} finally {
						// Wait to allow for standard latency (interval override
						// in spread sheet)
						delay();
					}

					if (expectedFailure() == false) {
						return;
					}
				} catch (AssertionFailedError e) {
					LOG.warn("IAM00031: The connector you are using incorrectly threw an AssertionFailedError. It needs to be updated to use an IAMUnitFailureException.");
					if (expectedFailure()) {
						return;
					}

					if (retriesRemaining > 0) {
						// If retries are enabled, retry the test after the
						// defined wait interval
						delay();
						LOG.info("RETRY (" + retriesRemaining + ") "
								+ e.getMessage() + " " + this.getName());
						continue;
					} else {
						LOG.info("...FAILURE: " + e.getMessage());
						logCriticalErrors(e.getMessage());
						throw e;
					}
				} catch (IAMUnitFailureException e) {
					if (expectedFailure()) {
						return;
					}

					if (retriesRemaining > 0) {
						// If retries are enabled, retry the test after the
						// defined wait interval
						delay();
						LOG.info("RETRY (" + retriesRemaining + ") "
								+ e.getMessage() + " " + this.getName());
						continue;
					} else {
						LOG.info("...FAILURE: " + e.getMessage());
						logCriticalErrors(e.getMessage());
						throw new AssertionFailedError("IAM00032: " + e.getMessage());
					}
				} catch (IAMUnitException e) {
					if (expectedFailure()) {
						return;
					}

					String msg = "";
					if (e.getMessage() != null && e.getCause() != null
							&& e.getCause().getMessage() != null) {
						msg = e.getMessage() + " " + e.getCause().getMessage();
					} else if (e.getMessage() != null) {
						msg = e.getMessage();
					} else if (e.getCause() != null
							&& e.getCause().getMessage() != null) {
						msg = e.getCause().getMessage();
					}

					if (retriesRemaining > 0) {
						// If retries are enabled, retry the test after the
						// defined wait interval
						delay();
						LOG.info("RETRY (" + retriesRemaining + ") " + msg
								+ " " + this.getName());
						continue;
					} else {
						LOG.info("...FAILURE: " + msg);
						// logCriticalErrors(msg);
						throw e;
					}
				}
				String msg = "Test failed: would have succeeded, but ExpectFailure was set to TRUE";
				if (retriesRemaining > 0) {
					// If retries are enabled, retry the test after the defined
					// wait interval
					delay();
					LOG.info("RETRY (" + retriesRemaining + ") " + msg + " "
							+ this.getName());
					continue;
				} else {
					LOG.info("...FAILURE: " + msg);
					logCriticalErrors(msg);
					fail(msg);
				}
			}
		} finally {
			connector.tearDown();
			connector = null;
		}
	}

	/**
	 * get the number of retries for this operation
	 *  
	 * @since v1.0.0
	 * @return Converted data map
	 **/
	private long getRetryCount() throws IAMUnitException {
		long maxRetryCount = 0;
		Object longObject = getOperationData(KEY_RETRIES);
		if (longObject != null) {
			try {
				String longVal = (String) longObject;
				// Strip whitespace
				longVal = longVal.trim();
				maxRetryCount = Long.parseLong(longVal);
			} catch (NumberFormatException e) {
				maxRetryCount = Long.parseLong(ConfigLoader.parms
						.get("maxretries"));
				LOG.error("Failed to parse " + KEY_RETRIES
						+ " into a value of type long.");
			}
		}
		if (this.configData != null
				&& (Long.parseLong(ConfigLoader.parms.get("multiplierWait")) > 0)) {
			return (maxRetryCount * Long.parseLong(ConfigLoader.parms
					.get("multiplierWait")));
		} else {
			return maxRetryCount;
		}
	}

	/**
	 * delays exectution for a while
	 * 
	 * @since v1.0.0
	 * @return Converted data map
	 **/
	private void delay() throws IAMUnitException {
		long delayInterval = Long.parseLong(ConfigLoader.parms
				.get("defaultdelay"));
		// check for delay interval override
		Object delayObject = getOperationData(KEY_INTERVAL);
		if (delayObject != null) {
			String delayVal = null;
			try {
				delayVal = (String) delayObject;
				// Trim whitespace
				delayVal = delayVal.trim();
				delayInterval = Long.parseLong(delayVal);
			} catch (NumberFormatException e) {
				LOG.error("IAM00033: Failed to parse " + KEY_INTERVAL
						+ " into a value of type long: " + e.getMessage()
						+ ": delayVal: [" + delayVal + "]");
			}
		}

		try {
			if (Long.parseLong(ConfigLoader.parms.get("multiplierWait")) > 0) {
				Thread.sleep((delayInterval * Long.parseLong(ConfigLoader.parms
						.get("multiplierWait"))));
			} else {
				Thread.sleep(delayInterval);
			}
		} catch (InterruptedException ie) {
			LOG.info("delay interrupted");
		}
	}

	/**
	 * sets up the connector specified in the name for use in the tests
	 * 
	 * @since v1.0.0
	 * @see Connector
	 * @param name
	 *            the name of the connector to setup
	 * @return Connector initialized Connector object
	 * @throws IAMUnitException
	 **/
	private Connector setupConnector(String name) throws IAMUnitException {
		if (configData.getType().isEmpty() == true) {
			throw new IAMUnitException("IAM00034: ...connection type is empty");
		} else {
			LOG.debug("...setupConnector: " + configData.getType());

			try {
				Connector connector = (Connector) Class.forName(
						configData.getType()).newInstance();
				connector.setup(configData.getParams());
				return connector;
			} catch (InstantiationException e) {
				LOG.info("error: " + e.getMessage());
				throw new IAMUnitException(
						"IAM00035: Failed to instantiate connection class of type: "
								+ configData.getType() + " " + e.getMessage());
			} catch (IllegalAccessException e) {
				LOG.info("error: " + e.getMessage());
				throw new IAMUnitException(
						"IAM00036: Illegal access error when attempting to instantiate connection of type: "
								+ configData.getType());
			} catch (ClassNotFoundException e) {
				LOG.info("error: " + e.getMessage());
				throw new IAMUnitException(
						"IAM00037: Specified target connection module not found: "
								+ configData.getType());
			}
		}
	}

	/**
	 * retrieves the value of the operation to be found
	 * 
	 * @since v1.0.0
	 * @param op
	 *            operation to find
	 * @return String value of the operation specified by op
	 **/
	private String getOperationData(String op) throws IAMUnitException {
		if (this.operationalDataMap == null) {
			throw new IAMUnitException(
					"IAM00038: No operation data found. This is most likely because the specified sheet does not exist in this workbook.");
		}
		String selectedOperation = (String) this.operationalDataMap.get(op);
		if ((selectedOperation == null || selectedOperation.length() < 1)
				&& op.equalsIgnoreCase("operation")) {
			fail("IAM00039: Missing operation - Please check the operation column for this row and try again.");
		}
		return selectedOperation;
	}

	/**
	 * retrieves the value of the operation to be found
	 * 
	 * @since v1.0.0
	 * @return String value of the operation specified by op
	 **/
	private String getOperation() throws IAMUnitException {
		if (operationalDataMap.get(KEY_OPERATION) == null)
			return "";
		else
			return operationalDataMap.get(KEY_OPERATION).toString();
	}

	/**
	 * converts data for use in the connector
	 * 
	 * @since v1.0.0
	 * @return Converted data map
	 **/
	@SuppressWarnings("rawtypes")
	private Map<String, Collection<String>> convertData() {
		Map<String, Collection<String>> data = new LinkedHashMap<String, Collection<String>>();

		Boolean subsfound = false;
		Map<String, String> subs = new HashMap<String, String>();
		subs = configData.getSubstitutions();
		if (subs != null) {
			subsfound = true;
		}

		Boolean schemaFound = false;
		Map<String, String> schema = new HashMap<String, String>();
		if (ConfigLoader.getSchema(configData.getName()) != null) {
			schemaFound = true;
			schema = stringToMap(ConfigLoader.getSchema(configData.getName()));
		}

		String delim = ConfigLoader.parms.get(PARM_DELIM);
		Iterator itr = attributeMap.keySet().iterator();
		while (itr.hasNext() == true) {
			String attrName = (String) itr.next();
			String attrVal = (String) attributeMap.get(attrName);

			if (subsfound == true) {
				Iterator subitr = subs.keySet().iterator();
				while (subitr.hasNext()) {
					String subName = (String) subitr.next();
					String subVal = (String) subs.get(subName);
					attrVal = attrVal.replace(subName, subVal);
				}
			}
			if (schemaFound == true) {
				String mappedName = (String) schema.get(attrName);
				if (mappedName != null) {
					attrName = mappedName;
				}
			}
			if (attrVal.indexOf(delim) == -1) {
				data.put(attrName, Arrays.asList(new String[] { attrVal }));
			} else {
				List<String> valueList = new ArrayList<String>();
				StringTokenizer tokenizer = new StringTokenizer(attrVal, delim);
				while (tokenizer.hasMoreTokens() == true) {
					valueList.add(tokenizer.nextToken().trim());
				}
				data.put(attrName, valueList);
			}
		}
		return data;
	}

	/**
	 * converts data for use in the connector
	 * 
	 * @since v1.0.0
	 * @return Converted data map
	 **/
	private boolean expectedFailure() throws IAMUnitException {
		boolean expectedFailure = false;
		if (operationalDataMap.get(KEY_FAIL) != null) {
			String s = operationalDataMap.get(KEY_FAIL).toString();
			if (s != null && s.length() > 0) {
				expectedFailure = Boolean.parseBoolean(s);
			}

		}
		return expectedFailure;
	}

	/**
	 * converts data for use in the connector
	 * 
	 * @since v1.0.0
	 * @return Converted data map
	 **/
	private static Map<String, String> stringToMap(String input) {
		Map<String, String> map = new HashMap<String, String>();

		input = input.replace("{", "");
		input = input.replace("}", "");
		input = input.replace(" ", "");
		String[] nameValuePairs = input.split(",");
		for (String nameValuePair : nameValuePairs) {
			String[] nameValue = nameValuePair.split("=");
			map.put(nameValue[0], nameValue[1]);
		}
		return map;
	}

	/**
	 * Allows for logging of critical errors and exiting if //iscritital
	 * operation attribute is true
	 * 
	 * @since v1.0.0
	 * @param str
	 *            Message to log
	 **/
	private void logCriticalErrors(String msg) throws IAMUnitException {
		Boolean isCritical = Boolean.valueOf(getOperationData(KEY_CRITICAL));
		if (isCritical) {
			LOG.info("...HALTING, this failure has been flagged critical:"
					+ msg);
			System.exit(0);
		}
	}

}
